from khronos.utils.misc import INF

class Trailable(object):
    """Implements a trailable object, i.e. an object with a stack of states that can be navigated 
    back and forth, like a browser's page state. A stack of states is maintained and when the 
    user wants to go back to a previous state he just needs to call back() or pop() and the 
    object configures its target automatically to the previous state. Additionally, states may be 
    given names which are easier to memorize and make client code mode readable."""
    def __init__(self, get=None, set=None):
        self.__states = []
        self.__aliases = {}
        self.__pos = -1
        self.__get = get
        self.__set = set
        
    def __repr__(self):
        return "<%s[%s, %s] at %s>" % (self.__class__.__name__, self.__pos, 
                                       self.__states[self.__pos][1], hex(id(self)))
        
    # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # FUNCTION DECORATORS
    def getter(self, fnc):
        self.__get = fnc
        return fnc
        
    def setter(self, fnc):
        self.__set = fnc
        return fnc
        
    # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    def current(self):
        state, name = self.__states[self.__pos]
        return self.__pos, name, state
        
    def clear(self):
        self.__states = []
        self.__aliases = {}
        self.__pos = -1
        
    def push(self, alias=None):
        """Push a new state on top of the current position in the stack. Upper states are 
        discarded."""
        self.__pos += 1
        data = [self.__get(), alias]
        if self.__pos == len(self.__states):
            self.__states.append(data)
        else:
            self.__states[self.__pos] = data
            self.discard_topmost(len(self.__states) - self.__pos - 1)
        if alias is not None:
            self.__aliases[alias] = self.__pos
            
    def pop(self):
        """From the current position, remove a state and go to the state below that one in the 
        stack. All upper states are discarded."""
        if self.__pos < 0:
            raise IndexError("unable to pop from empty state stack")
        self.__pos -= 1
        if self.__pos >= 0:
            self.__set(self.__states[self.__pos][0])
        self.discard_topmost(len(self.__states) - self.__pos - 1)
        
    def discard_topmost(self, n=1):
        """Discard n top-most states of the stack."""
        for _ in xrange(n):
            state, name = self.__states.pop()
            if name is not None:
                del self.__aliases[name]
        if self.__pos >= len(self.__states):
            self.set(len(self.__states) - 1)
            
    def back(self):
        """Go to the state below the current one, but keep upper states for later revisiting 
        them. This works just like a web browser's back functionality."""
        if self.__pos > 0:
            self.__pos -= 1
            self.__set(self.__states[self.__pos][0])
            
    def forward(self):
        """Go to the state above the current one (if possible). This works just like a web 
        browser's forward functionality."""
        if self.__pos < len(self.__states):
            self.__pos += 1
            self.__set(self.__states[self.__pos][0])
            
    def set(self, id=None):
        """Go to a given state."""
        if id is None:
            id = self.__pos
        try:
            state, name = self.__states[id]
            self.__pos = id
        except (IndexError, TypeError):
            pos = self.__aliases[id]
            state, name = self.__states[pos]
            self.__pos = pos
        self.__set(state)
        
    def update(self, id=None):
        """Save state to the given position or alias on the state stack."""
        if id is None:
            id = self.__pos
        state = self.__get()
        try:
            self.__states[id][0] = state
        except (IndexError, TypeError):
            pos = self.__aliases[id]
            self.__states[pos][0] = state
            
    # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    # ALIAS MANAGEMENT
    def alias(self, pos, name):
        if pos < 0 or pos >= len(self.__states):
            raise IndexError("index out of range")
        oldname = self.__states[pos][1]
        self.__states[pos][1] = name
        self.__aliases[name] = pos
        if oldname is not None:
            del self.__aliases[oldname]
            
    def unalias(self, name):
        pos = self.__aliases.pop(name)
        self.__states[pos][1] = None
        
    def show_aliases(self):
        print self
        for name, pos in self.__aliases.iteritems():
            print "\t", name, "->", pos
            
